// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector;

import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.uiDesigner.UIFormXmlConstants;
import com.intellij.util.SlowOperations;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.neuronbit.businessflow.intellij.editor.swing.XmlWriter;
import com.neuronbit.businessflow.intellij.editor.swing.editor.RadComponent;
import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutionException;

/**
 * The type Introspected property.
 *
 * @param <V> the type parameter
 */
public abstract class IntrospectedProperty<V> extends Property<RadComponent, V> {
    /**
     * The constant EMPTY_OBJECT_ARRAY.
     */
    protected final static Object[] EMPTY_OBJECT_ARRAY = new Object[]{};

    /**
     * This method is used to set property value to "delegee" JComponent
     */
    @NotNull
    protected final Method myReadMethod;
    /**
     * This method is used to get property value from "delegee" JComponent
     */
    @NotNull
    private final Method myWriteMethod;

    private final boolean myStoreAsClient;

    /**
     * The constant INTRO_PREFIX.
     */
    @NonNls
    public static final String INTRO_PREFIX = "Intro:";

    /**
     * Instantiates a new Introspected property.
     *
     * @param name          the name
     * @param readMethod    the read method
     * @param writeMethod   the write method
     * @param storeAsClient the store as client
     */
    public IntrospectedProperty(final String name, @NotNull final Method readMethod, @NotNull final Method writeMethod, final boolean storeAsClient) {
        super(null, name);
        myReadMethod = readMethod;
        myWriteMethod = writeMethod;
        myStoreAsClient = storeAsClient;
    }

    /**
     * <b>Do not overide this method without serious reason!</b>
     */
    @Override
    public V getValue(final RadComponent component) {
        //noinspection unchecked
        return (V) invokeGetter(component);
    }

    /**
     * Invoke getter object.
     *
     * @param component the component
     * @return the object
     */
    protected Object invokeGetter(final RadComponent component) {
        if (myStoreAsClient) {
            return component.getClientProperty(INTRO_PREFIX + getName());
        }
        try {
            myReadMethod.setAccessible(true);
            return myReadMethod.invoke(component.getDelegee(), EMPTY_OBJECT_ARRAY);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * <b>Do not overide this method without serious reason!</b>
     */
    @Override
    protected void setValueImpl(final RadComponent component, final V value) throws Exception {
        invokeSetter(component, value);
    }

    /**
     * Invoke setter.
     *
     * @param component the component
     * @param value     the value
     * @throws IllegalAccessException    the illegal access exception
     * @throws InvocationTargetException the invocation target exception
     */
    protected void invokeSetter(final RadComponent component, final Object value) throws IllegalAccessException, InvocationTargetException {
        if (myStoreAsClient) {
            component.putClientProperty(INTRO_PREFIX + getName(), value);
        } else {
            myWriteMethod.setAccessible(true);
            myWriteMethod.invoke(component.getDelegee(), value);
        }
    }

    /**
     * Serializes (writes) propertie's value
     *
     * @param value  property value which should be serialized.
     * @param writer writer which should be used for serialization. It is assumed that               before invocation of this method {@code writer} already has opened tag               that corresponds to this property. You can just append some attributes               here or add some subtags.
     */
    public void write(@NotNull V value, XmlWriter writer) {
        writer.addAttribute(UIFormXmlConstants.ATTRIBUTE_VALUE, value.toString());
    }

    @Override
    public boolean isModified(final RadComponent component) {
        return component.isMarkedAsModified(this);
    }

    @Override
    public void resetValue(RadComponent component) throws Exception {
        final V defaultValue = getDefaultValue(component.getDelegee());
        invokeSetter(component, defaultValue);
        markTopmostModified(component, false);
    }

    /**
     * Gets default value.
     *
     * @param delegee the delegee
     * @return the default value
     * @throws Exception the exception
     */
    protected V getDefaultValue(final Object delegee) throws Exception {
        if (myStoreAsClient) {
            return null;
        }
        throw new NotImplementedException();
//    final Constructor constructor = delegee.getClass().getConstructor(ArrayUtil.EMPTY_CLASS_ARRAY);
//    constructor.setAccessible(true);
//    JComponent newComponent = (JComponent)constructor.newInstance(ArrayUtilRt.EMPTY_OBJECT_ARRAY);
//    noinspection unchecked
//    return (V) myReadMethod.invoke(newComponent, EMPTY_OBJECT_ARRAY);
    }

    @Override
    public boolean appliesTo(final RadComponent component) {
        @NonNls String name = getName();
//    if (name.equals(SwingProperties.PREFERRED_SIZE) ||
//        name.equals(SwingProperties.MINIMUM_SIZE) ||
//        name.equals(SwingProperties.MAXIMUM_SIZE)) {
//      // our own properties must be used instead
//      final RadContainer parent = component.getParent();
//      return parent != null && !(parent.getLayoutManager() instanceof RadGridLayoutManager);
//    }

        // check if property is available in the JDK used by the module containing the component
        Project project = component.getProject();
        final PsiManager psiManager = PsiManager.getInstance(project);
        final GlobalSearchScope scope = component.getModule().getModuleWithDependenciesAndLibrariesScope(true);
        PsiClass componentClass = JavaPsiFacade.getInstance(psiManager.getProject()).findClass(component.getComponentClassName(), scope);
        if (componentClass == null) return true;
        try (AccessToken ignore = SlowOperations.knownIssue("IDEA-307701, EA-641435")) {
            final PsiMethod[] psiMethods = componentClass.findMethodsByName(myReadMethod.getName(), true);
            for (PsiMethod method : psiMethods) {
                if (!method.hasModifierProperty(PsiModifier.STATIC) &&
                        method.getParameterList().isEmpty()) {
                    return true;
                }
            }
        }
        return false;
    }
}
